Skip to content

docs: add callback retention example to Duplicating stubs#172

Merged
dimitropoulos merged 3 commits into
cloudflare:mainfrom
SAY-5:docs/callback-examples
Jul 27, 2026
Merged

docs: add callback retention example to Duplicating stubs#172
dimitropoulos merged 3 commits into
cloudflare:mainfrom
SAY-5:docs/callback-examples

Conversation

@SAY-5

@SAY-5 SAY-5 commented May 21, 2026

Copy link
Copy Markdown
Contributor

Refs #30.

In #30, @kentonv (member) said "I agree we could use more examples of callbacks." The follow-up comment showed the typical pitfall: a setCallback(cbfunc) method that stores cbfunc and invokes it later from a timer throws "Attempted to use RPC stub after it has been disposed" unless the stub is duplicated with .dup().

This adds a short subsection to "Duplicating stubs" in the README with:

  • the bidirectional-callback pattern that motivates .dup(),
  • a 12-line RpcTarget example that stores cbfunc.dup() and disposes it from [Symbol.dispose],
  • a note that the same rule applies in the reverse direction (client retaining a stub returned by the server).

README only. No code changes.

@changeset-bot

changeset-bot Bot commented May 21, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 872efaa

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actions Bot commented May 21, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@SAY-5

SAY-5 commented May 21, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@dimitropoulos
dimitropoulos force-pushed the docs/callback-examples branch from 21b1ebb to ee055de Compare July 26, 2026 20:58

@dimitropoulos dimitropoulos left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed a small leak and got it typechecking and otherwise looks good to go!

example code on the leak
// Failure mode #2: calling setCallback() twice leaks the previous timer AND the
// previous duplicated stub, because `this.timer` / `this.cbfunc` get overwritten.
//
// This uses a real RPC session (MessageChannel) so that `.dup()` and implicit
// param disposal behave exactly as they would over the wire.
//
// Run:
//   node fail-2-double-register-leak.ts
//
// Expected output (numbers approximate):
//   registered 3 times; server only tracks the last timer + last dup
//   after dispose(), orphaned intervals fired ~6 more times in 1s  <-- LEAK
//
// A correct implementation (see example.ts) calls a #stop() helper at the top of
// setCallback() so re-registration clears the old timer and disposes the old dup;
// after dispose() the count would be 0.

import { RpcTarget, newMessagePortRpcSession } from 'capnweb';

let liveTimerTicks = 0;

// The OLD, broken server exactly as written in PR #172 (field decls added only
// so Node's type-stripping accepts it; the logic is unchanged).
class Api extends RpcTarget {
  cbfunc: any;
  timer: any;

  async setCallback(cbfunc: any) {
    // BUG: no cleanup of any previously-registered callback.
    this.cbfunc = cbfunc.dup();
    this.timer = setInterval(() => {
      liveTimerTicks++;
      try { this.cbfunc('tick'); } catch { /* disposed stub */ }
    }, 300);
  }

  [Symbol.dispose]() {
    clearInterval(this.timer);      // clears ONLY the last timer
    this.cbfunc[Symbol.dispose]();  // disposes ONLY the last dup
  }
}

const { port1, port2 } = new MessageChannel();
const server = new Api();
newMessagePortRpcSession(port2 as any, server);
const api = newMessagePortRpcSession<Api>(port1 as any);

const makeCb = (name: string) => (msg: string) => { void name; void msg; };

await api.setCallback(makeCb('A'));
await api.setCallback(makeCb('B'));
await api.setCallback(makeCb('C')); // 3 registrations => 3 intervals + 3 dups
console.log('registered 3 times; server only tracks the last timer + last dup');

server[Symbol.dispose]();
liveTimerTicks = 0;
await new Promise((r) => setTimeout(r, 1000));

console.log(`after dispose(), orphaned intervals fired ~${liveTimerTicks} more times in 1s  <-- LEAK`);
process.exit(0);

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/cloudflare/capnweb@172

commit: 872efaa

Comment thread README.md Outdated
Invoke the retained callback from a separate RPC method so the example
stays within the RPC domain and centers on the dup()/dispose lifecycle.
Uses no runtime-specific timer types, so it type-checks on any runtime.
@dimitropoulos
dimitropoulos merged commit 6664f94 into cloudflare:main Jul 27, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants